#!/usr/bin/env python3
import sys
import os
import copy
import time
import json
import inspect
import requests
import configparser
from pathlib import Path
from datetime import datetime, timedelta
from urllib.parse import urlparse

CL_RESET = "\033[0m"
CL_RED = "\033[31m"
CL_GREEN = "\033[32m"
CL_YELLOW = "\033[33m"
CL_BLUE = "\033[34m"
CL_MAGENTA = "\033[35m"
CL_CYAN = "\033[36m"
CL_WHITE = "\033[37m"
CL_REVCOL = "\033[7m"
CL_BOLD = "\033[1m"

CONFIG_FILE = "config.ini"

def usage(script_name):
    print(f"usage: {script_name} <METHOD> <URI> <JSON_FILE>", file=sys.stderr)
    print(f"i.e. : {script_name} POST /api/v2/dmp/get_crowd_list dmp.json", file=sys.stderr)
    sys.exit(1)

def DLOG_IMPL(prefix, message):
    """
    打印带时间戳和调用位置的调试日志。
    示例输出: 251112 17:30:45 (mymodule.py:42) User clicked button
    """
    frame = sys._getframe(2)
    filename = frame.f_code.co_filename.split('/')[-1]  # 只取文件名，不带路径
    lineno = frame.f_lineno
    timestamp = datetime.now().strftime('%y%m%d %H:%M:%S')
    print(f"{prefix}{timestamp}| {filename}:{lineno}| {message}", file=sys.stderr)

def DLOG_INFO(message):
    DLOG_IMPL('I', message)
def DLOG_ERROR(message):
    DLOG_IMPL('E', message)
def DLOG_WARN(message):
    DLOG_IMPL('W', message)
def DLOG_FATAL(message):
    DLOG_IMPL('F', message)

def param_split(s: str) -> list[str]:
    """简单安全地分割带空格和双引号的命令字符串"""
    tokens = []
    current = []
    in_quotes = False
    i = 0
    n = len(s)

    while i < n:
        ch = s[i]
        if ch == '"':
            in_quotes = not in_quotes
        elif ch == ' ' and not in_quotes:
            if current:
                tokens.append(''.join(current))
                current = []
        else:
            current.append(ch)
        i += 1

    if current:
        tokens.append(''.join(current))
    
    return tokens

def validate_args(args):
    """校验命令行参数数量"""
    if not isinstance(args, list):
        raise TypeError("args must be a list")    

    # Step 1: 兼容处理单字符串
    if len(args) == 1 and ' ' in args[0]:
        try:
            normalized = param_split(args[0])
        except Exception as e:
            raise ValueError(f"Failed to parse command string: {args[0]!r}") from e
    else:
        normalized = args

    #  校验数量
    if len(normalized) < 3:
        DLOG_ERROR(f"Error: input param error, expected at least 3 arguments, got {len(normalized)}: {normalized!r}")
        usage(sys.argv[0])
        sys.exit(1)

    # 返回
    return normalized

def normalize_method(method_str):
    """标准化 HTTP 方法并校验合法性"""
    method = method_str.upper()
    allowed = {"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"}
    if method not in allowed:
        DLOG_ERROR(f"ERROR: {method} error")
        sys.exit(1)
    return method

# load .json file
def load_json_file(json_file_path):
    """加载并压缩 JSON 文件内容，若文件不存在则返回 None"""
    path_file = Path(json_file_path)
    if not path_file.is_file():
        return None
    try:
        with open(path_file, 'r', encoding='utf-8') as f:
            return json.load(f)  # 文件内容 --> Python对象(通常是 dict 或 list)
            # return json.dumps(data, separators=(',', ':'))  # 等效 jq -c: : Python对象-->JSON字符串
    except (json.JSONDecodeError, OSError) as e:
        print(f"[ERROR]: {json_file_path} unvalid: {e}", file=sys.stderr)
        sys.exit(1)

# load .ini file
def load_ini_file(config_file="config.ini"):
    if not os.path.exists(config_file):
        raise FileNotFoundError(f"Config file not found: {config_file}")
    config = configparser.ConfigParser()
    config.read(config_file, encoding="utf-8")
    return config

def build_header(conf, has_body):
    """构建请求头"""
    token = conf["header"].get("token", "")
    headers = {
        "Accept": "*/*",
        "Accept-Encoding": "gzip, deflate, br",
        "Access-Token": token,
        "Connection": "keep-alive",
        "User-Agent": "PostmanRuntime-ApipostRuntime/1.1.0"
    }
    if has_body:
        headers["Content-Type"] = "application/json"
    return headers

def get_nested_value(data, key_path):
    """
    根据 a.b.c 形式的 key_path 获取嵌套字典中的值。
    如果路径不存在，返回 None。
    """
    keys = key_path.split('.')
    current = data
    try:
        for k in keys:
            current = current[k]
        return current
    except (KeyError, TypeError):
        return None

def set_nested_value_if_exists(data, path, value):
    """仅当完整路径在 data 中存在时，才更新最后一级的值"""
    keys = path.split(".")
    current = data
    # 遍历到倒数第二层
    for key in keys[:-1]:
        if not isinstance(current, dict) or key not in current:
            return False
        current = current[key]
        if not isinstance(current, dict):
            return False
    # 检查最后一级 key 是否存在
    if keys[-1] in current:
        current[keys[-1]] = value
        return True
    return False

def parse_ini_value(ini_value: str):
    """根据前缀解析配置值的类型"""
    raw_value = ini_value.strip()

    # 处理：模板字符串
    if raw_value.startswith("str_tmpl:"):
        template_str = raw_value[len("str_tmpl:"):].strip()
        
        # 获取当前时间上下文
        now = datetime.now()
        replacements = {
            'timestamp': now.strftime('%Y%m%d_%H%M%S'),      # 20251112_164600
            'datetime': now.strftime('%Y-%m-%d %H:%M:%S'),   # 2025-11-12 16:46:00
            'date': now.strftime('%Y%m%d'),                  # 20251112
            'time': now.strftime('%H%M%S'),                  # 164600
            'year': now.strftime('%Y'),
            'month': now.strftime('%m'),
            'day': now.strftime('%d'),
            'hour': now.strftime('%H'),
            'minute': now.strftime('%M'),
            'second': now.strftime('%S'),
            'unixtime': str(int(now.timestamp())),
            'unixtime_ms': str(int(now.timestamp() * 1000)),
        }
        
        # 使用 string.Template 安全替换
        try:
            tmpl = string.Template(template_str)
            return tmpl.safe_substitute(replacements)
        except Exception as e:
            raise ValueError(f"Template parsing failed in '{raw_value}': {e}")    
    
    # 处理: 正常流程
    if raw_value.lower().startswith("time:"):
        arg = raw_value.split(":", 1)[1].strip().lower()
        if arg == "now_ts":
            return int(time.time())
        elif arg == "now_ms":
            return int(time.time() * 1_000)
        elif arg == "now_us":
            return int(time.time() * 1_000_000)

    if raw_value.startswith("int:"):
        try:
            return int(raw_value[4:].strip())
        except ValueError:
            raise ValueError(f"Invalid int value: {raw_value}")
    
    if raw_value.startswith("bool:"):
        val = raw_value[5:].strip().lower()
        if val in ("true", "1", "yes", "on"):
            return True
        elif val in ("false", "0", "no", "off"):
            return False
        else:
            raise ValueError(f"Invalid bool value: {raw_value}")
    
    if raw_value.startswith("float:"):
        try:
            return float(raw_value[6:].strip())
        except ValueError:
            raise ValueError(f"Invalid float value: {raw_value}")
    
    # 默认：字符串
    return raw_value

def _apply_config_inplace(json_obj, config_dict, section_name="config"):
    """
    原地应用配置到 json_obj(不拷贝!)
    要求 json_obj 是 dict 且可变
    """
    for key, value in config_dict.items():
        if not value or (isinstance(value, str) and not value.strip()):
            continue

        try:
            actual_value = parse_ini_value(value)
        except ValueError as e:
            print(f"[ERROR] Invalid {section_name} config value for '{key}': {e}", file=sys.stderr)
            sys.exit(1)

        if "." in key:
            old_value = get_nested_value(json_obj, key)
            if set_nested_value_if_exists(json_obj, key, actual_value):
                if old_value != actual_value:
                    print(f"+++: update field: [{section_name}] {key}: {old_value} -> {actual_value}")
            else:
                print(f"[WARN] Skipping non-existent item: [{section_name}] {key}", file=sys.stderr)
        else:
            if key in json_obj:
                old_value = json_obj[key]
                if old_value != actual_value:
                    json_obj[key] = actual_value
                    print(f"+++: update field: [{section_name}] {key}: {old_value} -> {actual_value}")


def build_json_text(json_dict, glob_dict, serv_dict):
    if not isinstance(json_dict, dict):
        return json_dict

    # 只做一次深拷贝
    result = copy.deepcopy(json_dict)

    # 原地应用配置（无额外拷贝）
    _apply_config_inplace(result, glob_dict, "global")
    _apply_config_inplace(result, serv_dict, "service")

    return result

def send_request(method, url, headers, body):
    """发送 HTTP 请求并输出响应体"""
    try:
        response = requests.request(
            method=method,
            url=url,
            headers=headers,
            data=body
        )
        print()  # 空行分隔
        print(response.text)
        print()
    except requests.RequestException as e:
        print(f"[ERROR]: Request failed: {e}", file=sys.stderr)
        sys.exit(1)


# ./apipost.py GET /api/v2/dmp/get_crowd_list files/dmp.json
def main():
    try:
        main_args = validate_args(sys.argv[1:])
        method, uri, json_file = main_args[:3]
    except ValueError as e:
        print(f"Config error: {e}", file=sys.stderr)
        sys.exit(1)

    # load ini file
    ini_cfg = load_ini_file(CONFIG_FILE)
    gw_url = ini_cfg.get("header", "gw_url", fallback="http://11.100.129.222:80")

    # check param: method
    method = normalize_method(method)
    if uri.startswith(('http://', 'https://')):
        url = uri
        parsed = urlparse(url)
        uri = parsed.path
    else:
        if uri.startswith('/'):
            url = f"{gw_url}{uri}"
        else:
            url = f"{gw_url}/{uri}"

    # get service_type
    service_type = "dmp"
    parts = []
    parts = [part for part in uri.split('/') if part]
    if len(parts) < 3:
        print(f"Error: uri={uri} has less than 3 parts", file=sys.stderr)
        sys.exit(1)
    service_type = parts[2]
    if json_file is None:
        json_file = f"files/{service_type}.json"

    # load config file
    json_data = load_json_file(json_file)

	# run app
    headers = build_header(ini_cfg, has_body=(json_data is not None))
    if "global" not in ini_cfg:
        raise ValueError("配置文件缺少 [global] 段")
    if service_type not in ini_cfg:
        available = ", ".join(ini_cfg.sections())
        raise ValueError(f"服务 {service_type} 未在配置中找到。可用服务: {available}")
    global_dict = dict(ini_cfg["global"])
    service_dict = dict(ini_cfg[service_type])
    json_body = build_json_text(json_data, global_dict, service_dict)
    json_body_str = json.dumps(json_body, separators=(',', ':'))

    # run app
    print(f"{method} {url} {json_file}\n")
    if json_data is not None:
        #rint(json.dumps(json_data, ensure_ascii=False, separators=(',', ':')))
        print(json_body_str)
    send_request(method, url, headers, json_body_str)

if __name__ == "__main__":
    main()
